/*
 * Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved.
 */
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id: EnvironmentCheck.java,v 1.2.4.1 2005/09/09 07:13:59 pvedula Exp $
 */
package com.sun.org.apache.xalan.internal.xslt;

import com.sun.org.apache.xalan.internal.utils.ObjectFactory;
import com.sun.org.apache.xalan.internal.utils.SecuritySupport;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Utility class to report simple information about the environment.
 * Simplistic reporting about certain classes found in your JVM may
 * help answer some FAQs for simple problems.
 *
 * <p>Usage-command line:
 * <code>
 * java com.sun.org.apache.xalan.internal.xslt.EnvironmentCheck [-out outFile]
 * </code></p>
 *
 * <p>Usage-from program:
 * <code>
 * boolean environmentOK =
 * (new EnvironmentCheck()).checkEnvironment(yourPrintWriter);
 * </code></p>
 *
 * <p>Usage-from stylesheet:
 * <code><pre>
 *    &lt;?xml version="1.0"?&gt;
 *    &lt;xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
 *        xmlns:xalan="http://xml.apache.org/xalan"
 *        exclude-result-prefixes="xalan"&gt;
 *    &lt;xsl:output indent="yes"/&gt;
 *    &lt;xsl:template match="/"&gt;
 *      &lt;xsl:copy-of select="xalan:checkEnvironment()"/&gt;
 *    &lt;/xsl:template&gt;
 *    &lt;/xsl:stylesheet&gt;
 * </pre></code></p>
 *
 * <p>Xalan users reporting problems are encouraged to use this class
 * to see if there are potential problems with their actual
 * Java environment <b>before</b> reporting a bug.  Note that you
 * should both check from the JVM/JRE's command line as well as
 * temporarily calling checkEnvironment() directly from your code,
 * since the classpath may differ (especially for servlets, etc).</p>
 *
 * <p>Also see http://xml.apache.org/xalan-j/faq.html</p>
 *
 * <p>Note: This class is pretty simplistic:
 * results are not necessarily definitive nor will it find all
 * problems related to environment setup.  Also, you should avoid
 * calling this in deployed production code, both because it is
 * quite slow and because it forces classes to get loaded.</p>
 *
 * <p>Note: This class explicitly has very limited compile-time
 * dependencies to enable easy compilation and usage even when
 * Xalan, DOM/SAX/JAXP, etc. are not present.</p>
 *
 * <p>Note: for an improved version of this utility, please see
 * the xml-commons' project Which utility which does the same kind
 * of thing but in a much simpler manner.</p>
 *
 * @author Shane_Curcuru@us.ibm.com
 * @version $Id: EnvironmentCheck.java,v 1.10 2010-11-01 04:34:13 joehw Exp $
 */
public class EnvironmentCheck {

    /**
     * Command line runnability: checks for [-out outFilename] arg.
     * <p>Command line entrypoint; Sets output and calls
     * {@link #checkEnvironment(PrintWriter)}.</p>
     *
     * @param args command line args
     */
    public static void main(String[] args) {
        // Default to System.out, autoflushing
        PrintWriter sendOutputTo = new PrintWriter(System.out, true);

        // Read our simplistic input args, if supplied
        for (int i = 0; i < args.length; i++) {
            if ("-out".equalsIgnoreCase(args[i])) {
                i++;

                if (i < args.length) {
                    try {
                        sendOutputTo = new PrintWriter(new FileWriter(args[i], true));
                    } catch (Exception e) {
                        System.err.println("# WARNING: -out " + args[i] + " threw "
                                + e.toString());
                    }
                } else {
                    System.err.println(
                            "# WARNING: -out argument should have a filename, output sent to console");
                }
            }
        }

        EnvironmentCheck app = new EnvironmentCheck();
        app.checkEnvironment(sendOutputTo);
    }

    /**
     * Programmatic entrypoint: Report on basic Java environment
     * and CLASSPATH settings that affect Xalan.
     *
     * <p>Note that this class is not advanced enough to tell you
     * everything about the environment that affects Xalan, and
     * sometimes reports errors that will not actually affect
     * Xalan's behavior.  Currently, it very simplistically
     * checks the JVM's environment for some basic properties and
     * logs them out; it will report a problem if it finds a setting
     * or .jar file that is <i>likely</i> to cause problems.</p>
     *
     * <p>Advanced users can peruse the code herein to help them
     * investigate potential environment problems found; other users
     * may simply send the output from this tool along with any bugs
     * they submit to help us in the debugging process.</p>
     *
     * @param pw PrintWriter to send output to; can be sent to a
     *           file that will look similar to a Properties file; defaults
     *           to System.out if null
     * @return true if your environment appears to have no major
     * problems; false if potential environment problems found
     * @see #getEnvironmentHash()
     */
    public boolean checkEnvironment(PrintWriter pw) {

        // Use user-specified output writer if non-null
        if (null != pw)
            outWriter = pw;

        // Setup a hash to store various environment information in
        Map<String, Object> hash = getEnvironmentHash();

        // Check for ERROR keys in the hashtable, and print report
        boolean environmentHasErrors = writeEnvironmentReport(hash);

        if (environmentHasErrors) {
            // Note: many logMsg calls have # at the start to
            //  fake a property-file like output
            logMsg("# WARNING: Potential problems found in your environment!");
            logMsg("#    Check any 'ERROR' items above against the Xalan FAQs");
            logMsg("#    to correct potential problems with your classes/jars");
            logMsg("#    http://xml.apache.org/xalan-j/faq.html");
            if (null != outWriter)
                outWriter.flush();
            return false;
        } else {
            logMsg("# YAHOO! Your environment seems to be OK.");
            if (null != outWriter)
                outWriter.flush();
            return true;
        }
    }

    /**
     * Fill a hash with basic environment settings that affect Xalan.
     *
     * <p>Worker method called from various places.</p>
     * <p>Various system and CLASSPATH, etc. properties are put into
     * the hash as keys with a brief description of the current state
     * of that item as the value.  Any serious problems will be put in
     * with a key that is prefixed with {@link #ERROR 'ERROR.'} so it
     * stands out in any resulting report; also a key with just that
     * constant will be set as well for any error.</p>
     * <p>Note that some legitimate cases are flaged as potential
     * errors - namely when a developer recompiles xalan.jar on their
     * own - and even a non-error state doesn't guaruntee that
     * everything in the environment is correct.  But this will help
     * point out the most common classpath and system property
     * problems that we've seen.</p>
     *
     * @return Map full of useful environment info about Xalan and related
     * system properties, etc.
     */
    public Map<String, Object> getEnvironmentHash() {
        // Setup a hash to store various environment information in
        Map<String, Object> hash = new HashMap<>();

        // Call various worker methods to fill in the hash
        //  These are explicitly separate for maintenance and so
        //  advanced users could call them standalone
        checkJAXPVersion(hash);
        checkProcessorVersion(hash);
        checkParserVersion(hash);
        checkAntVersion(hash);
        if (!checkDOML3(hash)) {
            checkDOMVersion(hash);
        }
        checkSAXVersion(hash);
        checkSystemProperties(hash);

        return hash;
    }

    /**
     * Dump a basic Xalan environment report to outWriter.
     *
     * <p>This dumps a simple header and then each of the entries in
     * the Map to our PrintWriter; it does special processing
     * for entries that are .jars found in the classpath.</p>
     *
     * @param h Map of items to report on; presumably
     *          filled in by our various check*() methods
     * @return true if your environment appears to have no major
     * problems; false if potential environment problems found
     * @see #appendEnvironmentReport(Node, Document, Map)
     * for an equivalent that appends to a Node instead
     */
    protected boolean writeEnvironmentReport(Map<String, Object> h) {

        if (null == h) {
            logMsg("# ERROR: writeEnvironmentReport called with null Map");
            return false;
        }

        boolean errors = false;

        logMsg(
                "#---- BEGIN writeEnvironmentReport($Revision: 1.10 $): Useful stuff found: ----");

        // Fake the Properties-like output
        for (Map.Entry<String, Object> entry : h.entrySet()) {
            String keyStr = entry.getKey();
            try {
                // Special processing for classes found..
                if (keyStr.startsWith(FOUNDCLASSES)) {
                    List<Map> v = (ArrayList<Map>) entry.getValue();
                    errors |= logFoundJars(v, keyStr);
                }
                // ..normal processing for all other entries
                else {
                    // Note: we could just check for the ERROR key by itself,
                    //    since we now set that, but since we have to go
                    //    through the whole hash anyway, do it this way,
                    //    which is safer for maintenance
                    if (keyStr.startsWith(ERROR)) {
                        errors = true;
                    }
                    logMsg(keyStr + "=" + h.get(keyStr));
                }
            } catch (Exception e) {
                logMsg("Reading-" + keyStr + "= threw: " + e.toString());
            }
        }

        logMsg(
                "#----- END writeEnvironmentReport: Useful properties found: -----");

        return errors;
    }

    /**
     * Prefixed to hash keys that signify serious problems.
     */
    public static final String ERROR = "ERROR.";

    /**
     * Added to descriptions that signify potential problems.
     */
    public static final String WARNING = "WARNING.";

    /**
     * Value for any error found.
     */
    public static final String ERROR_FOUND = "At least one error was found!";

    /**
     * Prefixed to hash keys that signify version numbers.
     */
    public static final String VERSION = "version.";

    /**
     * Prefixed to hash keys that signify .jars found in classpath.
     */
    public static final String FOUNDCLASSES = "foundclasses.";

    /**
     * Marker that a class or .jar was found.
     */
    public static final String CLASS_PRESENT = "present-unknown-version";

    /**
     * Marker that a class or .jar was not found.
     */
    public static final String CLASS_NOTPRESENT = "not-present";

    /**
     * Listing of common .jar files that include Xalan-related classes.
     */
    public String[] jarNames =
            {
                    "xalan.jar", "xalansamples.jar", "xalanj1compat.jar", "xalanservlet.jar",
                    "serializer.jar",   // Serializer (shared between Xalan & Xerces)
                    "xerces.jar",       // Xerces-J 1.x
                    "xercesImpl.jar",   // Xerces-J 2.x
                    "testxsl.jar",
                    "crimson.jar",
                    "lotusxsl.jar",
                    "jaxp.jar", "parser.jar", "dom.jar", "sax.jar", "xml.jar",
                    "xml-apis.jar",
                    "xsltc.jar"
            };

    /**
     * Print out report of .jars found in a classpath.
     * <p>
     * Takes the information encoded from a checkPathForJars()
     * call and dumps it out to our PrintWriter.
     *
     * @param v    List of Maps of .jar file info
     * @param desc description to print out in header
     * @return false if OK, true if any .jars were reported
     * as having errors
     * @see #checkPathForJars(String, String[])
     */
    protected boolean logFoundJars(List<Map> v, String desc) {

        if ((null == v) || (v.size() < 1))
            return false;

        boolean errors = false;

        logMsg("#---- BEGIN Listing XML-related jars in: " + desc + " ----");

        for (Map<String, String> v1 : v) {
            for (Map.Entry<String, String> entry : v1.entrySet()) {
                String keyStr = entry.getKey();
                try {
                    if (keyStr.startsWith(ERROR)) {
                        errors = true;
                    }
                    logMsg(keyStr + "=" + entry.getValue());

                } catch (Exception e) {
                    errors = true;
                    logMsg("Reading-" + keyStr + "= threw: " + e.toString());
                }
            }
        }

        logMsg("#----- END Listing XML-related jars in: " + desc + " -----");

        return errors;
    }

    /**
     * Stylesheet extension entrypoint: Dump a basic Xalan
     * environment report from getEnvironmentHash() to a Node.
     *
     * <p>Copy of writeEnvironmentReport that creates a Node suitable
     * for other processing instead of a properties-like text output.
     * </p>
     *
     * @param container Node to append our report to
     * @param factory   Document providing createElement, etc. services
     * @param h         Hash presumably from {@link #getEnvironmentHash()}
     * @see #writeEnvironmentReport(Map)
     * for an equivalent that writes to a PrintWriter instead
     */
    public void appendEnvironmentReport(Node container, Document factory, Map<String, Object> h) {
        if ((null == container) || (null == factory)) {
            return;
        }

        try {
            Element envCheckNode = factory.createElement("EnvironmentCheck");
            envCheckNode.setAttribute("version", "$Revision: 1.10 $");
            container.appendChild(envCheckNode);

            if (null == h) {
                Element statusNode = factory.createElement("status");
                statusNode.setAttribute("result", "ERROR");
                statusNode.appendChild(factory.createTextNode("appendEnvironmentReport called with null Map!"));
                envCheckNode.appendChild(statusNode);
                return;
            }

            boolean errors = false;

            Element hashNode = factory.createElement("environment");
            envCheckNode.appendChild(hashNode);

            for (Map.Entry<String, Object> entry : h.entrySet()) {
                String keyStr = entry.getKey();
                try {
                    // Special processing for classes found..
                    if (keyStr.startsWith(FOUNDCLASSES)) {
                        List<Map> v = (List<Map>) entry.getValue();
                        // errors |= logFoundJars(v, keyStr);
                        errors |= appendFoundJars(hashNode, factory, v, keyStr);
                    } // ..normal processing for all other entries
                    else {
                        // Note: we could just check for the ERROR key by itself,
                        //    since we now set that, but since we have to go
                        //    through the whole hash anyway, do it this way,
                        //    which is safer for maintenance
                        if (keyStr.startsWith(ERROR)) {
                            errors = true;
                        }
                        Element node = factory.createElement("item");
                        node.setAttribute("key", keyStr);
                        node.appendChild(factory.createTextNode((String) h.get(keyStr)));
                        hashNode.appendChild(node);
                    }
                } catch (Exception e) {
                    errors = true;
                    Element node = factory.createElement("item");
                    node.setAttribute("key", keyStr);
                    node.appendChild(factory.createTextNode(ERROR + " Reading " + keyStr + " threw: " + e.toString()));
                    hashNode.appendChild(node);
                }
            } // end of for...

            Element statusNode = factory.createElement("status");
            statusNode.setAttribute("result", (errors ? "ERROR" : "OK"));
            envCheckNode.appendChild(statusNode);
        } catch (Exception e2) {
            System.err.println("appendEnvironmentReport threw: " + e2.toString());
            e2.printStackTrace();
        }
    }

    /**
     * Print out report of .jars found in a classpath.
     * <p>
     * Takes the information encoded from a checkPathForJars()
     * call and dumps it out to our PrintWriter.
     *
     * @param container Node to append our report to
     * @param factory   Document providing createElement, etc. services
     * @param v         Map of Maps of .jar file info
     * @param desc      description to print out in header
     * @return false if OK, true if any .jars were reported
     * as having errors
     * @see #checkPathForJars(String, String[])
     */
    protected boolean appendFoundJars(Node container, Document factory,
                                      List<Map> v, String desc) {

        if ((null == v) || (v.size() < 1))
            return false;

        boolean errors = false;

        for (Map<String, String> v1 : v) {
            for (Map.Entry<String, String> entry : v1.entrySet()) {
                String keyStr = entry.getKey();
                try {
                    if (keyStr.startsWith(ERROR)) {
                        errors = true;
                    }
                    Element node = factory.createElement("foundJar");
                    node.setAttribute("name", keyStr.substring(0, keyStr.indexOf("-")));
                    node.setAttribute("desc", keyStr.substring(keyStr.indexOf("-") + 1));
                    node.appendChild(factory.createTextNode(entry.getValue()));
                    container.appendChild(node);
                } catch (Exception e) {
                    errors = true;
                    Element node = factory.createElement("foundJar");
                    node.appendChild(factory.createTextNode(ERROR + " Reading " + keyStr + " threw: " + e.toString()));
                    container.appendChild(node);
                }
            }
        }
        return errors;
    }

    /**
     * Fillin hash with info about SystemProperties.
     * <p>
     * Logs java.class.path and other likely paths; then attempts
     * to search those paths for .jar files with Xalan-related classes.
     * <p>
     * //@todo NOTE: We don't actually search java.ext.dirs for
     * //  *.jar files therein! This should be updated
     *
     * @param h Map to put information in
     * @see #jarNames
     * @see #checkPathForJars(String, String[])
     */
    protected void checkSystemProperties(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        // Grab java version for later use
        try {
            String javaVersion = SecuritySupport.getSystemProperty("java.version");

            h.put("java.version", javaVersion);
        } catch (SecurityException se) {

            // For applet context, etc.
            h.put(
                    "java.version",
                    "WARNING: SecurityException thrown accessing system version properties");
        }

        // Printout jar files on classpath(s) that may affect operation
        //  Do this in order
        try {

            // This is present in all JVM's
            String cp = SecuritySupport.getSystemProperty("java.class.path");

            h.put("java.class.path", cp);

            List<Map> classpathJars = checkPathForJars(cp, jarNames);

            if (null != classpathJars) {
                h.put(FOUNDCLASSES + "java.class.path", classpathJars);
            }

            // Also check for JDK 1.2+ type classpaths
            String othercp = SecuritySupport.getSystemProperty("sun.boot.class.path");

            if (null != othercp) {
                h.put("sun.boot.class.path", othercp);
                classpathJars = checkPathForJars(othercp, jarNames);

                if (null != classpathJars) {
                    h.put(FOUNDCLASSES + "sun.boot.class.path", classpathJars);
                }
            }

            //@todo NOTE: We don't actually search java.ext.dirs for
            //  *.jar files therein! This should be updated
            othercp = SecuritySupport.getSystemProperty("java.ext.dirs");

            if (null != othercp) {
                h.put("java.ext.dirs", othercp);

                classpathJars = checkPathForJars(othercp, jarNames);

                if (null != classpathJars)
                    h.put(FOUNDCLASSES + "java.ext.dirs", classpathJars);
            }

            //@todo also check other System properties' paths?
            //  v2 = checkPathForJars(System.getProperty("sun.boot.library.path"), jarNames);   // ?? may not be needed
            //  v3 = checkPathForJars(System.getProperty("java.library.path"), jarNames);   // ?? may not be needed
        } catch (SecurityException se2) {
            // For applet context, etc.
            h.put(
                    "java.class.path",
                    "WARNING: SecurityException thrown accessing system classpath properties");
        }
    }

    /**
     * Cheap-o listing of specified .jars found in the classpath.
     * <p>
     * cp should be separated by the usual File.pathSeparator.  We
     * then do a simplistic search of the path for any requested
     * .jar filenames, and return a listing of their names and
     * where (apparently) they came from.
     *
     * @param cp   classpath to search
     * @param jars array of .jar base filenames to look for
     * @return List of Maps filled with info about found .jars
     * @see #jarNames
     * @see #logFoundJars(Map, String)
     * @see #appendFoundJars(Node, Document, Map, String)
     * @see #getApparentVersion(String, long)
     */
    protected List<Map> checkPathForJars(String cp, String[] jars) {

        if ((null == cp) || (null == jars) || (0 == cp.length())
                || (0 == jars.length))
            return null;

        List<Map> v = new ArrayList<>();
        StringTokenizer st = new StringTokenizer(cp, File.pathSeparator);

        while (st.hasMoreTokens()) {

            // Look at each classpath entry for each of our requested jarNames
            String filename = st.nextToken();

            for (int i = 0; i < jars.length; i++) {
                if (filename.indexOf(jars[i]) > -1) {
                    File f = new File(filename);

                    if (f.exists()) {

                        // If any requested jarName exists, report on
                        //  the details of that .jar file
                        try {
                            Map<String, String> h = new HashMap<>(2);
                            // Note "-" char is looked for in appendFoundJars
                            h.put(jars[i] + "-path", f.getAbsolutePath());

                            // We won't bother reporting on the xalan.jar apparent version
                            // since this requires knowing the jar size of the xalan.jar
                            // before we build it.
                            // For other jars, eg. xml-apis.jar and xercesImpl.jar, we
                            // report the apparent version of the file we've found
                            if (!("xalan.jar".equalsIgnoreCase(jars[i]))) {
                                h.put(jars[i] + "-apparent.version",
                                        getApparentVersion(jars[i], f.length()));
                            }
                            v.add(h);
                        } catch (Exception e) {

                            /* no-op, don't add it  */
                        }
                    } else {
                        Map<String, String> h = new HashMap<>(2);
                        // Note "-" char is looked for in appendFoundJars
                        h.put(jars[i] + "-path", WARNING + " Classpath entry: "
                                + filename + " does not exist");
                        h.put(jars[i] + "-apparent.version", CLASS_NOTPRESENT);
                        v.add(h);
                    }
                }
            }
        }

        return v;
    }

    /**
     * Cheap-o method to determine the product version of a .jar.
     * <p>
     * Currently does a lookup into a local table of some recent
     * shipped Xalan builds to determine where the .jar probably
     * came from.  Note that if you recompile Xalan or Xerces
     * yourself this will likely report a potential error, since
     * we can't certify builds other than the ones we ship.
     * Only reports against selected posted Xalan-J builds.
     * <p>
     * //@todo actually look up version info in manifests
     *
     * @param jarName base filename of the .jarfile
     * @param jarSize size of the .jarfile
     * @return String describing where the .jar file probably
     * came from
     */
    protected String getApparentVersion(String jarName, long jarSize) {
        // If we found a matching size and it's for our
        //  jar, then return it's description
        // Lookup in static JARVERSIONS Map
        String foundSize = JARVERSIONS.get(new Long(jarSize));

        if ((null != foundSize) && (foundSize.startsWith(jarName))) {
            return foundSize;
        } else {
            if ("xerces.jar".equalsIgnoreCase(jarName)
                    || "xercesImpl.jar".equalsIgnoreCase(jarName))
//              || "xalan.jar".equalsIgnoreCase(jarName))
            {

                // For xalan.jar and xerces.jar/xercesImpl.jar, which we ship together:
                // The jar is not from a shipped copy of xalan-j, so
                //  it's up to the user to ensure that it's compatible
                return jarName + " " + WARNING + CLASS_PRESENT;
            } else {

                // Otherwise, it's just a jar we don't have the version info calculated for
                return jarName + " " + CLASS_PRESENT;
            }
        }
    }

    /**
     * Report version information about JAXP interfaces.
     * <p>
     * Currently distinguishes between JAXP 1.0.1 and JAXP 1.1,
     * and not found; only tests the interfaces, and does not
     * check for reference implementation versions.
     *
     * @param h Map to put information in
     */
    protected void checkJAXPVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        Class clazz = null;

        try {
            final String JAXP1_CLASS = "javax.xml.stream.XMLStreamConstants";

            clazz = ObjectFactory.findProviderClass(JAXP1_CLASS, true);

            // If we succeeded, we have JAXP 1.4 available
            h.put(VERSION + "JAXP", "1.4");
        } catch (Exception e) {
            h.put(ERROR + VERSION + "JAXP", "1.3");
            h.put(ERROR, ERROR_FOUND);
        }
    }

    /**
     * Report product version information from Xalan-J.
     * <p>
     * Looks for version info in xalan.jar from Xalan-J products.
     *
     * @param h Map to put information in
     */
    protected void checkProcessorVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        try {
            final String XALAN1_VERSION_CLASS =
                    "com.sun.org.apache.xalan.internal.xslt.XSLProcessorVersion";

            Class clazz = ObjectFactory.findProviderClass(XALAN1_VERSION_CLASS, true);

            // Found Xalan-J 1.x, grab it's version fields
            StringBuffer buf = new StringBuffer();
            Field f = clazz.getField("PRODUCT");

            buf.append(f.get(null));
            buf.append(';');

            f = clazz.getField("LANGUAGE");

            buf.append(f.get(null));
            buf.append(';');

            f = clazz.getField("S_VERSION");

            buf.append(f.get(null));
            buf.append(';');
            h.put(VERSION + "xalan1", buf.toString());
        } catch (Exception e1) {
            h.put(VERSION + "xalan1", CLASS_NOTPRESENT);
        }

        try {
            // NOTE: This is the old Xalan 2.0, 2.1, 2.2 version class,
            //    is being replaced by class below
            final String XALAN2_VERSION_CLASS =
                    "com.sun.org.apache.xalan.internal.processor.XSLProcessorVersion";

            Class clazz = ObjectFactory.findProviderClass(XALAN2_VERSION_CLASS, true);

            // Found Xalan-J 2.x, grab it's version fields
            StringBuffer buf = new StringBuffer();
            Field f = clazz.getField("S_VERSION");
            buf.append(f.get(null));

            h.put(VERSION + "xalan2x", buf.toString());
        } catch (Exception e2) {
            h.put(VERSION + "xalan2x", CLASS_NOTPRESENT);
        }
        try {
            // NOTE: This is the new Xalan 2.2+ version class
            final String XALAN2_2_VERSION_CLASS =
                    "com.sun.org.apache.xalan.internal.Version";
            final String XALAN2_2_VERSION_METHOD = "getVersion";
            final Class noArgs[] = new Class[0];

            Class clazz = ObjectFactory.findProviderClass(XALAN2_2_VERSION_CLASS, true);

            Method method = clazz.getMethod(XALAN2_2_VERSION_METHOD, noArgs);
            Object returnValue = method.invoke(null, new Object[0]);

            h.put(VERSION + "xalan2_2", (String) returnValue);
        } catch (Exception e2) {
            h.put(VERSION + "xalan2_2", CLASS_NOTPRESENT);
        }
    }

    /**
     * Report product version information from common parsers.
     * <p>
     * Looks for version info in xerces.jar/xercesImpl.jar/crimson.jar.
     * <p>
     * //@todo actually look up version info in crimson manifest
     *
     * @param h Map to put information in
     */
    protected void checkParserVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        try {
            final String XERCES1_VERSION_CLASS = "com.sun.org.apache.xerces.internal.framework.Version";

            Class clazz = ObjectFactory.findProviderClass(XERCES1_VERSION_CLASS, true);

            // Found Xerces-J 1.x, grab it's version fields
            Field f = clazz.getField("fVersion");
            String parserVersion = (String) f.get(null);

            h.put(VERSION + "xerces1", parserVersion);
        } catch (Exception e) {
            h.put(VERSION + "xerces1", CLASS_NOTPRESENT);
        }

        // Look for xerces1 and xerces2 parsers separately
        try {
            final String XERCES2_VERSION_CLASS = "com.sun.org.apache.xerces.internal.impl.Version";

            Class clazz = ObjectFactory.findProviderClass(XERCES2_VERSION_CLASS, true);

            // Found Xerces-J 2.x, grab it's version fields
            Field f = clazz.getField("fVersion");
            String parserVersion = (String) f.get(null);

            h.put(VERSION + "xerces2", parserVersion);
        } catch (Exception e) {
            h.put(VERSION + "xerces2", CLASS_NOTPRESENT);
        }

        try {
            final String CRIMSON_CLASS = "org.apache.crimson.parser.Parser2";

            Class clazz = ObjectFactory.findProviderClass(CRIMSON_CLASS, true);

            //@todo determine specific crimson version
            h.put(VERSION + "crimson", CLASS_PRESENT);
        } catch (Exception e) {
            h.put(VERSION + "crimson", CLASS_NOTPRESENT);
        }
    }

    /**
     * Report product version information from Ant.
     *
     * @param h Map to put information in
     */
    protected void checkAntVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        try {
            final String ANT_VERSION_CLASS = "org.apache.tools.ant.Main";
            final String ANT_VERSION_METHOD = "getAntVersion"; // noArgs
            final Class noArgs[] = new Class[0];

            Class clazz = ObjectFactory.findProviderClass(ANT_VERSION_CLASS, true);

            Method method = clazz.getMethod(ANT_VERSION_METHOD, noArgs);
            Object returnValue = method.invoke(null, new Object[0]);

            h.put(VERSION + "ant", (String) returnValue);
        } catch (Exception e) {
            h.put(VERSION + "ant", CLASS_NOTPRESENT);
        }
    }

    /**
     * Report version info from DOM interfaces.
     *
     * @param h Map to put information in
     */
    protected boolean checkDOML3(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        final String DOM_CLASS = "org.w3c.dom.Document";
        final String DOM_LEVEL3_METHOD = "getDoctype";  // no parameter

        try {
            Class clazz = ObjectFactory.findProviderClass(DOM_CLASS, true);

            Method method = clazz.getMethod(DOM_LEVEL3_METHOD, (Class<?>[]) null);

            // If we succeeded, we have loaded interfaces from a
            //  level 3 DOM somewhere
            h.put(VERSION + "DOM", "3.0");
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Report version info from DOM interfaces.
     * <p>
     * Currently distinguishes between pre-DOM level 2, the DOM
     * level 2 working draft, the DOM level 2 final draft,
     * and not found.
     *
     * @param h Map to put information in
     */
    protected void checkDOMVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        final String DOM_LEVEL2_CLASS = "org.w3c.dom.Document";
        final String DOM_LEVEL2_METHOD = "createElementNS";  // String, String
        final String DOM_LEVEL3_METHOD = "getDoctype";  // no parameter
        final String DOM_LEVEL2WD_CLASS = "org.w3c.dom.Node";
        final String DOM_LEVEL2WD_METHOD = "supported";  // String, String
        final String DOM_LEVEL2FD_CLASS = "org.w3c.dom.Node";
        final String DOM_LEVEL2FD_METHOD = "isSupported";  // String, String
        final Class twoStringArgs[] = {java.lang.String.class,
                java.lang.String.class};

        try {
            Class clazz = ObjectFactory.findProviderClass(DOM_LEVEL2_CLASS, true);

            Method method = clazz.getMethod(DOM_LEVEL2_METHOD, twoStringArgs);

            // If we succeeded, we have loaded interfaces from a
            //  level 2 DOM somewhere
            h.put(VERSION + "DOM", "2.0");

            try {
                // Check for the working draft version, which is
                //  commonly found, but won't work anymore
                clazz = ObjectFactory.findProviderClass(DOM_LEVEL2WD_CLASS, true);

                method = clazz.getMethod(DOM_LEVEL2WD_METHOD, twoStringArgs);

                h.put(ERROR + VERSION + "DOM.draftlevel", "2.0wd");
                h.put(ERROR, ERROR_FOUND);
            } catch (Exception e2) {
                try {
                    // Check for the final draft version as well
                    clazz = ObjectFactory.findProviderClass(DOM_LEVEL2FD_CLASS, true);

                    method = clazz.getMethod(DOM_LEVEL2FD_METHOD, twoStringArgs);

                    h.put(VERSION + "DOM.draftlevel", "2.0fd");
                } catch (Exception e3) {
                    h.put(ERROR + VERSION + "DOM.draftlevel", "2.0unknown");
                    h.put(ERROR, ERROR_FOUND);
                }
            }
        } catch (Exception e) {
            h.put(ERROR + VERSION + "DOM",
                    "ERROR attempting to load DOM level 2 class: " + e.toString());
            h.put(ERROR, ERROR_FOUND);
        }

        //@todo load an actual DOM implmementation and query it as well
        //@todo load an actual DOM implmementation and check if
        //  isNamespaceAware() == true, which is needed to parse
        //  xsl stylesheet files into a DOM
    }

    /**
     * Report version info from SAX interfaces.
     * <p>
     * Currently distinguishes between SAX 2, SAX 2.0beta2,
     * SAX1, and not found.
     *
     * @param h Map to put information in
     */
    protected void checkSAXVersion(Map<String, Object> h) {

        if (null == h)
            h = new HashMap<>();

        final String SAX_VERSION1_CLASS = "org.xml.sax.Parser";
        final String SAX_VERSION1_METHOD = "parse";  // String
        final String SAX_VERSION2_CLASS = "org.xml.sax.XMLReader";
        final String SAX_VERSION2_METHOD = "parse";  // String
        final String SAX_VERSION2BETA_CLASSNF = "org.xml.sax.helpers.AttributesImpl";
        final String SAX_VERSION2BETA_METHODNF = "setAttributes";  // Attributes
        final Class oneStringArg[] = {java.lang.String.class};
        // Note this introduces a minor compile dependency on SAX...
        final Class attributesArg[] = {org.xml.sax.Attributes.class};

        try {
            // This method was only added in the final SAX 2.0 release;
            //  see changes.html "Changes from SAX 2.0beta2 to SAX 2.0prerelease"
            Class clazz = ObjectFactory.findProviderClass(SAX_VERSION2BETA_CLASSNF, true);

            Method method = clazz.getMethod(SAX_VERSION2BETA_METHODNF, attributesArg);

            // If we succeeded, we have loaded interfaces from a
            //  real, final SAX version 2.0 somewhere
            h.put(VERSION + "SAX", "2.0");
        } catch (Exception e) {
            // If we didn't find the SAX 2.0 class, look for a 2.0beta2
            h.put(ERROR + VERSION + "SAX",
                    "ERROR attempting to load SAX version 2 class: " + e.toString());
            h.put(ERROR, ERROR_FOUND);

            try {
                Class clazz = ObjectFactory.findProviderClass(SAX_VERSION2_CLASS, true);

                Method method = clazz.getMethod(SAX_VERSION2_METHOD, oneStringArg);

                // If we succeeded, we have loaded interfaces from a
                //  SAX version 2.0beta2 or earlier; these might work but
                //  you should really have the final SAX 2.0
                h.put(VERSION + "SAX-backlevel", "2.0beta2-or-earlier");
            } catch (Exception e2) {
                // If we didn't find the SAX 2.0beta2 class, look for a 1.0 one
                h.put(ERROR + VERSION + "SAX",
                        "ERROR attempting to load SAX version 2 class: " + e.toString());
                h.put(ERROR, ERROR_FOUND);

                try {
                    Class clazz = ObjectFactory.findProviderClass(SAX_VERSION1_CLASS, true);

                    Method method = clazz.getMethod(SAX_VERSION1_METHOD, oneStringArg);

                    // If we succeeded, we have loaded interfaces from a
                    //  SAX version 1.0 somewhere; which won't work very
                    //  well for JAXP 1.1 or beyond!
                    h.put(VERSION + "SAX-backlevel", "1.0");
                } catch (Exception e3) {
                    // If we didn't find the SAX 2.0 class, look for a 1.0 one
                    // Note that either 1.0 or no SAX are both errors
                    h.put(ERROR + VERSION + "SAX-backlevel",
                            "ERROR attempting to load SAX version 1 class: " + e3.toString());

                }
            }
        }
    }

    /**
     * Manual table of known .jar sizes.
     * Only includes shipped versions of certain projects.
     * key=jarsize, value=jarname ' from ' distro name
     * Note assumption: two jars cannot have the same size!
     *
     * @see #getApparentVersion(String, long)
     */
    private static final Map<Long, String> JARVERSIONS;

    /**
     * Static initializer for JARVERSIONS table.
     * Doing this just once saves time and space.
     *
     * @see #getApparentVersion(String, long)
     */
    static {
        Map<Long, String> jarVersions = new HashMap<>();
        jarVersions.put(new Long(857192), "xalan.jar from xalan-j_1_1");
        jarVersions.put(new Long(440237), "xalan.jar from xalan-j_1_2");
        jarVersions.put(new Long(436094), "xalan.jar from xalan-j_1_2_1");
        jarVersions.put(new Long(426249), "xalan.jar from xalan-j_1_2_2");
        jarVersions.put(new Long(702536), "xalan.jar from xalan-j_2_0_0");
        jarVersions.put(new Long(720930), "xalan.jar from xalan-j_2_0_1");
        jarVersions.put(new Long(732330), "xalan.jar from xalan-j_2_1_0");
        jarVersions.put(new Long(872241), "xalan.jar from xalan-j_2_2_D10");
        jarVersions.put(new Long(882739), "xalan.jar from xalan-j_2_2_D11");
        jarVersions.put(new Long(923866), "xalan.jar from xalan-j_2_2_0");
        jarVersions.put(new Long(905872), "xalan.jar from xalan-j_2_3_D1");
        jarVersions.put(new Long(906122), "xalan.jar from xalan-j_2_3_0");
        jarVersions.put(new Long(906248), "xalan.jar from xalan-j_2_3_1");
        jarVersions.put(new Long(983377), "xalan.jar from xalan-j_2_4_D1");
        jarVersions.put(new Long(997276), "xalan.jar from xalan-j_2_4_0");
        jarVersions.put(new Long(1031036), "xalan.jar from xalan-j_2_4_1");
        // Stop recording xalan.jar sizes as of Xalan Java 2.5.0

        jarVersions.put(new Long(596540), "xsltc.jar from xalan-j_2_2_0");
        jarVersions.put(new Long(590247), "xsltc.jar from xalan-j_2_3_D1");
        jarVersions.put(new Long(589914), "xsltc.jar from xalan-j_2_3_0");
        jarVersions.put(new Long(589915), "xsltc.jar from xalan-j_2_3_1");
        jarVersions.put(new Long(1306667), "xsltc.jar from xalan-j_2_4_D1");
        jarVersions.put(new Long(1328227), "xsltc.jar from xalan-j_2_4_0");
        jarVersions.put(new Long(1344009), "xsltc.jar from xalan-j_2_4_1");
        jarVersions.put(new Long(1348361), "xsltc.jar from xalan-j_2_5_D1");
        // Stop recording xsltc.jar sizes as of Xalan Java 2.5.0

        jarVersions.put(new Long(1268634), "xsltc.jar-bundled from xalan-j_2_3_0");

        jarVersions.put(new Long(100196), "xml-apis.jar from xalan-j_2_2_0 or xalan-j_2_3_D1");
        jarVersions.put(new Long(108484), "xml-apis.jar from xalan-j_2_3_0, or xalan-j_2_3_1 from xml-commons-1.0.b2");
        jarVersions.put(new Long(109049), "xml-apis.jar from xalan-j_2_4_0 from xml-commons RIVERCOURT1 branch");
        jarVersions.put(new Long(113749), "xml-apis.jar from xalan-j_2_4_1 from factoryfinder-build of xml-commons RIVERCOURT1");
        jarVersions.put(new Long(124704), "xml-apis.jar from tck-jaxp-1_2_0 branch of xml-commons");
        jarVersions.put(new Long(124724), "xml-apis.jar from tck-jaxp-1_2_0 branch of xml-commons, tag: xml-commons-external_1_2_01");
        jarVersions.put(new Long(194205), "xml-apis.jar from head branch of xml-commons, tag: xml-commons-external_1_3_02");

        // If the below were more common I would update it to report
        //  errors better; but this is so old hardly anyone has it
        jarVersions.put(new Long(424490), "xalan.jar from Xerces Tools releases - ERROR:DO NOT USE!");

        jarVersions.put(new Long(1591855), "xerces.jar from xalan-j_1_1 from xerces-1...");
        jarVersions.put(new Long(1498679), "xerces.jar from xalan-j_1_2 from xerces-1_2_0.bin");
        jarVersions.put(new Long(1484896), "xerces.jar from xalan-j_1_2_1 from xerces-1_2_1.bin");
        jarVersions.put(new Long(804460), "xerces.jar from xalan-j_1_2_2 from xerces-1_2_2.bin");
        jarVersions.put(new Long(1499244), "xerces.jar from xalan-j_2_0_0 from xerces-1_2_3.bin");
        jarVersions.put(new Long(1605266), "xerces.jar from xalan-j_2_0_1 from xerces-1_3_0.bin");
        jarVersions.put(new Long(904030), "xerces.jar from xalan-j_2_1_0 from xerces-1_4.bin");
        jarVersions.put(new Long(904030), "xerces.jar from xerces-1_4_0.bin");
        jarVersions.put(new Long(1802885), "xerces.jar from xerces-1_4_2.bin");
        jarVersions.put(new Long(1734594), "xerces.jar from Xerces-J-bin.2.0.0.beta3");
        jarVersions.put(new Long(1808883), "xerces.jar from xalan-j_2_2_D10,D11,D12 or xerces-1_4_3.bin");
        jarVersions.put(new Long(1812019), "xerces.jar from xalan-j_2_2_0");
        jarVersions.put(new Long(1720292), "xercesImpl.jar from xalan-j_2_3_D1");
        jarVersions.put(new Long(1730053), "xercesImpl.jar from xalan-j_2_3_0 or xalan-j_2_3_1 from xerces-2_0_0");
        jarVersions.put(new Long(1728861), "xercesImpl.jar from xalan-j_2_4_D1 from xerces-2_0_1");
        jarVersions.put(new Long(972027), "xercesImpl.jar from xalan-j_2_4_0 from xerces-2_1");
        jarVersions.put(new Long(831587), "xercesImpl.jar from xalan-j_2_4_1 from xerces-2_2");
        jarVersions.put(new Long(891817), "xercesImpl.jar from xalan-j_2_5_D1 from xerces-2_3");
        jarVersions.put(new Long(895924), "xercesImpl.jar from xerces-2_4");
        jarVersions.put(new Long(1010806), "xercesImpl.jar from Xerces-J-bin.2.6.2");
        jarVersions.put(new Long(1203860), "xercesImpl.jar from Xerces-J-bin.2.7.1");

        jarVersions.put(new Long(37485), "xalanj1compat.jar from xalan-j_2_0_0");
        jarVersions.put(new Long(38100), "xalanj1compat.jar from xalan-j_2_0_1");

        jarVersions.put(new Long(18779), "xalanservlet.jar from xalan-j_2_0_0");
        jarVersions.put(new Long(21453), "xalanservlet.jar from xalan-j_2_0_1");
        jarVersions.put(new Long(24826), "xalanservlet.jar from xalan-j_2_3_1 or xalan-j_2_4_1");
        jarVersions.put(new Long(24831), "xalanservlet.jar from xalan-j_2_4_1");
        // Stop recording xalanservlet.jar sizes as of Xalan Java 2.5.0; now a .war file

        // For those who've downloaded JAXP from sun
        jarVersions.put(new Long(5618), "jaxp.jar from jaxp1.0.1");
        jarVersions.put(new Long(136133), "parser.jar from jaxp1.0.1");
        jarVersions.put(new Long(28404), "jaxp.jar from jaxp-1.1");
        jarVersions.put(new Long(187162), "crimson.jar from jaxp-1.1");
        jarVersions.put(new Long(801714), "xalan.jar from jaxp-1.1");
        jarVersions.put(new Long(196399), "crimson.jar from crimson-1.1.1");
        jarVersions.put(new Long(33323), "jaxp.jar from crimson-1.1.1 or jakarta-ant-1.4.1b1");
        jarVersions.put(new Long(152717), "crimson.jar from crimson-1.1.2beta2");
        jarVersions.put(new Long(88143), "xml-apis.jar from crimson-1.1.2beta2");
        jarVersions.put(new Long(206384), "crimson.jar from crimson-1.1.3 or jakarta-ant-1.4.1b1");

        // jakarta-ant: since many people use ant these days
        jarVersions.put(new Long(136198), "parser.jar from jakarta-ant-1.3 or 1.2");
        jarVersions.put(new Long(5537), "jaxp.jar from jakarta-ant-1.3 or 1.2");

        JARVERSIONS = Collections.unmodifiableMap(jarVersions);
    }

    /**
     * Simple PrintWriter we send output to; defaults to System.out.
     */
    protected PrintWriter outWriter = new PrintWriter(System.out, true);

    /**
     * Bottleneck output: calls outWriter.println(s).
     *
     * @param s String to print
     */
    protected void logMsg(String s) {
        outWriter.println(s);
    }
}
